// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
//
// Pinmux toplevel.
//

`include "prim_assert.sv"

module pinmux import pinmux_pkg::*; import pinmux_reg_pkg::*; (
  input                            clk_i,
  input                            rst_ni,
  // Slow always-on clock
  input                            clk_aon_i,
  input                            rst_aon_ni,
  // Wakeup request, running on clk_aon_i
  output logic                     aon_wkup_req_o,
  // Sleep enable, running on clk_i
  input                            sleep_en_i,
  // Strap sample request
  input  lc_strap_req_t            lc_pinmux_strap_i,
  output lc_strap_rsp_t            lc_pinmux_strap_o,
  // Bus Interface (device)
  input  tlul_pkg::tl_h2d_t        tl_i,
  output tlul_pkg::tl_d2h_t        tl_o,
  // Muxed Peripheral side
  input        [NMioPeriphOut-1:0] periph_to_mio_i,
  input        [NMioPeriphOut-1:0] periph_to_mio_oe_i,
  output logic [NMioPeriphIn-1:0]  mio_to_periph_o,
  // Dedicated Peripheral side
  input        [NDioPads-1:0]      periph_to_dio_i,
  input        [NDioPads-1:0]      periph_to_dio_oe_i,
  output logic [NDioPads-1:0]      dio_to_periph_o,
  // Pad side
  // MIOs
  output logic [NMioPads-1:0]      mio_out_o,
  output logic [NMioPads-1:0]      mio_oe_o,
  input        [NMioPads-1:0]      mio_in_i,
  // DIOs
  output logic [NDioPads-1:0]      dio_out_o,
  output logic [NDioPads-1:0]      dio_oe_o,
  input        [NDioPads-1:0]      dio_in_i
);

  ////////////////////////////
  // Parameters / Constants //
  ////////////////////////////

  // TODO: these need to be parameterizable via topgen at some point.
  // They have been placed here such that they do not generate
  // warnings in the C header generation step, since logic is not supported
  // as a data type yet.
  localparam logic [pinmux_reg_pkg::NMioPeriphOut-1:0] MioPeriphHasSleepMode
                   = {pinmux_reg_pkg::NMioPeriphOut{1'b1}};
  localparam logic [pinmux_reg_pkg::NDioPads-1:0]      DioPeriphHasSleepMode
                   = {pinmux_reg_pkg::NDioPads{1'b1}};
  localparam logic [pinmux_reg_pkg::NDioPads-1:0]      DioPeriphHasWkup
                   = {pinmux_reg_pkg::NDioPads{1'b1}};

  //////////////////////////////////
  // Regfile Breakout and Mapping //
  //////////////////////////////////

  pinmux_reg2hw_t reg2hw;
  pinmux_hw2reg_t hw2reg;

  pinmux_reg_top u_reg (
    .clk_i  ,
    .rst_ni ,
    .tl_i   ,
    .tl_o   ,
    .reg2hw ,
    .hw2reg ,
    .devmode_i(1'b1)
  );

  /////////////////////
  // Sleep registers //
  /////////////////////

  logic sleep_en_q;
  logic [NMioPads-1:0] mio_out_sleep_d, mio_oe_sleep_d;
  logic [NMioPads-1:0] mio_out_sleep_q, mio_oe_sleep_q;
  logic [NDioPads-1:0] dio_out_sleep_d, dio_oe_sleep_d;
  logic [NDioPads-1:0] dio_out_sleep_q, dio_oe_sleep_q;
  // these are external due to their WARL behavior
  logic [NDioPads-1:0][1:0] dio_out_sleep_val_d, dio_out_sleep_val_q;

  // latch MIO/DIO state when going to sleep
  // 0: drive low
  // 1: drive high
  // 2: high-z
  // 3: previous value
  for (genvar k = 0; k < NMioPads; k++) begin : gen_mio_sleep
    assign mio_out_sleep_d[k] = (reg2hw.mio_out_sleep_val[k].q == 0) ? 1'b0 :
                                (reg2hw.mio_out_sleep_val[k].q == 1) ? 1'b1 :
                                (reg2hw.mio_out_sleep_val[k].q == 2) ? 1'b0 : mio_out_o[k];

    assign mio_oe_sleep_d[k] = (reg2hw.mio_out_sleep_val[k].q == 0) ? 1'b1 :
                               (reg2hw.mio_out_sleep_val[k].q == 1) ? 1'b1 :
                               (reg2hw.mio_out_sleep_val[k].q == 2) ? 1'b0 : mio_oe_o[k];
  end

  // since DIO pads are permanently mapped to a specific peripheral,
  // we only need to support retention regs on non-always on peripherals,
  // outputs / inouts.
  for (genvar k = 0; k < NDioPads; k++) begin : gen_dio_sleep
    if (DioPeriphHasSleepMode[k]) begin : gen_warl_connect
      assign hw2reg.dio_out_sleep_val[k].d = dio_out_sleep_val_q[k];

      assign dio_out_sleep_val_d[k] = (reg2hw.dio_out_sleep_val[k].qe) ?
                                      reg2hw.dio_out_sleep_val[k].q :
                                      dio_out_sleep_val_q[k];

      assign dio_out_sleep_d[k] = (dio_out_sleep_val_q[k] == 0) ? 1'b0 :
                                  (dio_out_sleep_val_q[k] == 1) ? 1'b1 :
                                  (dio_out_sleep_val_q[k] == 2) ? 1'b0 : dio_out_o[k];

      assign dio_oe_sleep_d[k] = (dio_out_sleep_val_q[k] == 0) ? 1'b1 :
                                 (dio_out_sleep_val_q[k] == 1) ? 1'b1 :
                                 (dio_out_sleep_val_q[k] == 2) ? 1'b0 : dio_oe_o[k];
    end else begin : gen_warl_tie0
      // these signals will be unused
      assign hw2reg.dio_out_sleep_val[k].d = 2'b10; // default value defined in hjson
      assign dio_out_sleep_val_d[k] = 2'b10; // default value defined in hjson
      assign dio_out_sleep_d[k]     = '0;
      assign dio_oe_sleep_d[k]      = '0;
    end
  end

  always_ff @(posedge clk_i or negedge rst_ni) begin : p_sleep
    if (!rst_ni) begin
      sleep_en_q          <= 1'b0;
      dio_out_sleep_val_q <= {NDioPads{2'b10}}; // default value defined in hjson
      mio_out_sleep_q     <= '0;
      mio_oe_sleep_q      <= '0;
      dio_out_sleep_q     <= '0;
      dio_oe_sleep_q      <= '0;
    end else begin
      sleep_en_q          <= sleep_en_i;
      dio_out_sleep_val_q <= dio_out_sleep_val_d;

      if (sleep_en_i & !sleep_en_q) begin
        mio_out_sleep_q <= mio_out_sleep_d;
        mio_oe_sleep_q  <= mio_oe_sleep_d;
        dio_out_sleep_q <= dio_out_sleep_d;
        dio_oe_sleep_q  <= dio_oe_sleep_d;
      end
    end
  end

  ///////////////
  // Input Mux //
  ///////////////

  for (genvar k = 0; k < NMioPeriphIn; k++) begin : gen_mio_periph_in
    logic [2**$clog2(NMioPads+2)-1:0] data_mux;
    // stack input and default signals for convenient indexing below
    // possible defaults: constant 0 or 1
    assign data_mux = $bits(data_mux)'({mio_in_i, 1'b1, 1'b0});
    // index using configured insel
    assign mio_to_periph_o[k] = data_mux[reg2hw.periph_insel[k].q];
  end

  ////////////////
  // Output Mux //
  ////////////////

  for (genvar k = 0; k < NMioPads; k++) begin : gen_mio_out
    logic sleep_en;
    logic [2**$clog2(NMioPeriphOut+3)-1:0] data_mux, oe_mux, sleep_mux;
    // stack output data/enable and default signals for convenient indexing below
    // possible defaults: 0, 1 or 2 (high-Z)
    assign data_mux  = $bits(data_mux)'({periph_to_mio_i, 1'b0, 1'b1, 1'b0});
    assign oe_mux    = $bits(oe_mux)'({periph_to_mio_oe_i,  1'b0, 1'b1, 1'b1});
    assign sleep_mux = $bits(sleep_mux)'({MioPeriphHasSleepMode,  1'b1, 1'b1, 1'b1});

    // check whether this peripheral can actually go to sleep
    assign sleep_en = sleep_mux[reg2hw.mio_outsel[k].q] & sleep_en_q;
    // index using configured outsel
    assign mio_out_o[k] = (sleep_en) ? mio_out_sleep_q[k] : data_mux[reg2hw.mio_outsel[k].q];
    assign mio_oe_o[k]  = (sleep_en) ? mio_oe_sleep_q[k]  : oe_mux[reg2hw.mio_outsel[k].q];
  end

  /////////////////////
  // DIO connections //
  /////////////////////

  // Inputs are just fed through
  assign dio_to_periph_o = dio_in_i;

  for (genvar k = 0; k < NDioPads; k++) begin : gen_dio_out
    // Since this is a DIO, this can be determined at design time
    if (DioPeriphHasSleepMode[k]) begin : gen_sleep
      assign dio_out_o[k] = (sleep_en_q) ? dio_out_sleep_q[k] : periph_to_dio_i[k];
      assign dio_oe_o[k]  = (sleep_en_q) ? dio_oe_sleep_q[k]  : periph_to_dio_oe_i[k];
    end else begin : gen_nosleep
      assign dio_out_o[k] = periph_to_dio_i[k];
      assign dio_oe_o[k]  = periph_to_dio_oe_i[k];
    end
  end

  //////////////////////
  // Wakeup detectors //
  //////////////////////

  localparam int AlignedMuxSize = (NMioPads + 2 > NDioPads) ? 2**$clog2(NMioPads + 2) :
                                                              2**$clog2(NDioPads);
  logic [NWkupDetect-1:0] aon_wkup_req;
  logic [AlignedMuxSize-1:0] dio_data_mux, mio_data_mux;
  assign mio_data_mux = AlignedMuxSize'({mio_in_i, 1'b0, 1'b0});

  // Only connect DIOs that are not excempt
  for (genvar k = 0; k < NDioPads; k++) begin : gen_dio_wkup
    if (DioPeriphHasWkup[k]) begin : gen_dio_wkup_connect
      assign dio_data_mux[k] = dio_in_i[k];
    end else begin : gen_dio_wkup_tie_off
      assign dio_data_mux[k] = 1'b0;
    end
  end

  for (genvar k = NDioPads; k < AlignedMuxSize; k++) begin : gen_dio_data_mux_tie_off
      assign dio_data_mux[k] = 1'b0;
  end

  for (genvar k = 0; k < NWkupDetect; k++) begin : gen_wkup_detect
    logic pin_value;
    assign pin_value = (reg2hw.wkup_detector[k].miodio.q)           ?
                       dio_data_mux[reg2hw.wkup_detector_padsel[k]] :
                       mio_data_mux[reg2hw.wkup_detector_padsel[k]];

    pinmux_wkup i_pinmux_wkup (
      .clk_i,
      .rst_ni,
      .clk_aon_i,
      .rst_aon_ni,
      // config signals. these are synched to clk_aon internally
      .wkup_en_i          ( reg2hw.wkup_detector_en[k].q                ),
      .filter_en_i        ( reg2hw.wkup_detector[k].filter.q            ),
      .wkup_mode_i        ( wkup_mode_e'(reg2hw.wkup_detector[k].mode.q)),
      .wkup_cnt_th_i      ( reg2hw.wkup_detector_cnt_th[k].q            ),
      .pin_value_i        ( pin_value                                   ),
      // cause reg signals. these are synched from/to clk_aon internally
      .wkup_cause_valid_i ( reg2hw.wkup_cause[k].qe                     ),
      .wkup_cause_data_i  ( reg2hw.wkup_cause[k].q                      ),
      .wkup_cause_data_o  ( hw2reg.wkup_cause[k].d                      ),
      // wakeup request signals on clk_aon (level encoded)
      .aon_wkup_req_o     ( aon_wkup_req[k]                             )
    );
  end

  // OR' together all wakeup requests
  assign aon_wkup_req_o = |aon_wkup_req;

  ////////////////////
  // Strap Sampling //
  ////////////////////

  logic [NStraps-1:0] lc_strap_taps;
  lc_strap_rsp_t lc_strap_d, lc_strap_q;

  for (genvar k = 0; k < NStraps; k++) begin : gen_strap_taps
    assign lc_strap_taps[k] = mio_in_i[MioStrapPos[k]];
  end

  assign lc_pinmux_strap_o = lc_strap_q;
  assign lc_strap_d = (lc_pinmux_strap_i.sample_pulse)      ?
                      '{valid: 1'b1, straps: lc_strap_taps} :
                      lc_strap_q;

  always_ff @(posedge clk_i or negedge rst_ni) begin : p_strap_sample
    if (!rst_ni) begin
      lc_strap_q <= '0;
    end else begin
      lc_strap_q <= lc_strap_d;
    end
  end

  ////////////////
  // Assertions //
  ////////////////

  `ASSERT_KNOWN(TlDValidKnownO_A, tl_o.d_valid)
  `ASSERT_KNOWN(TlAReadyKnownO_A, tl_o.a_ready)
  // `ASSERT_KNOWN(MioToPeriphKnownO_A, mio_to_periph_o)
  `ASSERT_KNOWN(MioOeKnownO_A, mio_oe_o)
  // `ASSERT_KNOWN(DioToPeriphKnownO_A, dio_to_periph_o)
  `ASSERT_KNOWN(DioOeKnownO_A, dio_oe_o)
  `ASSERT_KNOWN(LcPinmuxStrapKnownO_A, lc_pinmux_strap_o)

  // TODO: need to check why some outputs are not valid (e.g. SPI device MISO)
  // for (genvar k = 0; k < NMioPads; k++) begin : gen_mio_known_if
  //   `ASSERT_KNOWN_IF(MioOutKnownO_A, mio_out_o[k], mio_oe_o[k])
  // end

  // for (genvar k = 0; k < NDioPads; k++) begin : gen_dio_known_if
  //   `ASSERT_KNOWN_IF(DioOutKnownO_A, dio_out_o[k], dio_oe_o[k])
  // end

  // running on slow AON clock
  `ASSERT_KNOWN(AonWkupReqKnownO_A, aon_wkup_req_o, !rst_aon_ni, clk_aon_i)

endmodule : pinmux
